class DB: pass
from pymongo import Connection
from pymongo.master_slave_connection import MasterSlaveConnection
from pymongo.objectid import ObjectId
from pymongo.dbref import DBRef
from datetime import datetime

def connect(collection, host='localhost', port=27017, slaves=None):
	"Connect to MongoDB"
	if slaves and type(slaves) == list:
		slave_connections = []
		for slave in slaves:
			if type(slave) == tuple and len(slave) == 2:
				slave_host, slave_port = slave
				slave_connections.append(Connection(
						slave_host, slave_port, slave_okay=True))
			else:
				slave_connections.append(Connection(slave, slave_okay=True))
		master = Connection(host, port)
		DB.connection = MasterSlaveConnection(
			master, slave_connections)[collection]
	else:
		DB.connection = Connection(host, port)[collection]


def dict_unicode(d):
	"Utility method to convert dicts to use str keys"
	if not d: return {}
	ret = {}
	for k in d:
		ret[str(k)] = d[k]
	return ret
	 
class Field(object):
	"Persistable data field"
	def type_check(self, value):
		"Default validation against defined type"
		if issubclass(self.type, _ModelBase) and isinstance(value, dict):
			value = self.type(**dict_unicode(value))
		elif isinstance(value, datetime) and value != None:
			value = datetime(value.year, value.month, value.day, value.hour, \
			value.minute, value.second, value.microsecond/1000*1000)
		elif not isinstance(value, self.type):
			if self.required:
				raise ValueError('Required field left empty')
			elif value != None:
				raise TypeError('Expected %s: got %s' %
						(self.type, type(value)))
		return value
	def __init__(self, type, validator=None, default=None,
			hidden=False, required=False):
		"Takes some Python types?, and optionally a default value"
		self.type     = type
		self.default  = default
		self.hidden   = hidden
		self.required = required
		if not validator:
			self.validator = self.type_check
		else:
			self.validator = validator
	def __repr__(self):
		return 'This is an unset %s field.' % self.type
	@property
	def value(self):
		if callable(self.default):
			return self.default()
		else:return self.default

class ListField(Field):
	"Persistable data list field"
	def type_check(self, value):
		l = []
		if hasattr(value, '__iter__'):
			for v in value:
				l.append(super(ListField, self).type_check(v))
		return l
	def __init__(self, list, *args, **kwargs):
		super(ListField, self).__init__(list, *args, **kwargs)

class Cursor(object):
	"Iterable that creates object types"
	def __init__(self, cls, data, kwargs):
		self.cls     = cls #class is a no-no word
		self._data   = data
		self._kwargs = kwargs
		self._index  = None
	def __getitem__(self, index):
		self._index = index
		ret = self._data[index]
		if isinstance(ret, dict):
			return self.cls(**dict_unicode(ret))
		else:
			return self
	def __iter__(self): return self
	def next(self):
		return self.cls(**dict_unicode(self._data.next()))
	def sort(self, *args, **kwargs):
		self._data = self._data.sort(*args, **kwargs)
		return self
	def count(self, *args, **kwargs):
		return self._data.count()
	def where(self, *args, **kwargs):
		self._data = self._data.where(*args, **kwargs)
		return self
#	@property # Not a property due to poor implementation of hasattr
	def to_data(self):
#		return [o.to_data() for o in self] # Testing purposes
		cursor = DB.connection[self.cls.__name__.lower()].find(
			self._kwargs, self.cls._shown_fields())
		if self._index: cursor = cursor[self._index]
		return list(cursor)
	def delete(self):
		for o in self:
			o.delete() 
	def find(self, *args, **more_kwargs):
		kwargs = self._kwargs.copy()
		if more_kwargs: kwargs.update(more_kwargs)
		if len(args) and type(args[0]) == dict: kwargs.update(args[0])
		return self.cls.find(kwargs)
	def __repr__(self):
		return '<Cursor[%s]>\n\t%s\n</Cursor>' % (
			str(self._kwargs)[:60], '\n\t'.join(str(s) for s in self))

class _ModelBase(object):
	def __init__(self, **kwargs):
		"takes keyword arguments to pre-populate the object _fields"
		self._fields = self._fields()
		for f in self._fields:
			setattr(self.__class__, f, property(self._getter(f),
					self._setter(f, self._fields[f].validator)))
			if f in kwargs:
				try:
					setattr(self, f, kwargs[f])
				except: pass
			else:   setattr(self, f, self._fields[f].value)
		if '_id' in kwargs: self._id = kwargs['_id']
	def _getter(self, name):
		def _get(self):
			return self._fields[name]
		return _get
	def _setter(self, name, validator):
		def _set(self, value):
			self._fields[name] = validator(value)
			return self._fields[name]
		return _set
	@classmethod
	def _fields(cls):
		"Returns a list of Field objects from Model subclass"
		if not hasattr(cls, '_field_cache'):
			cls._field_cache = {}
			for f in cls.__dict__:
				if isinstance(cls.__dict__[f], Field):
					cls._field_cache[f] = cls.__dict__[f]
		return cls._field_cache.copy()
	@classmethod
	def _shown_fields(cls):
		return [k for k,v in cls._fields().iteritems() if not v.hidden]
#	@property # Removed due to poor hasattr implementation
#	def data(self):
#		return self._data()
	@staticmethod
	def _parse_data(input, show_hidden=False, use_id=True):
		if isinstance(input, list):
			return [_ModelBase._parse_data(l, show_hidden,
					use_id) for l in input]
		elif isinstance(input, SubModel):
			return input.to_data(show_hidden=show_hidden, use_id=use_id)
		elif isinstance(input, Model):
			return input.id
		else:
			return input
	def to_data(self, show_hidden=False, use_id=False):
		"Returns internal data as a dictionary"
		d = {}
		for f in self._fields:
			field = self._fields[f]
			if show_hidden or f in self._shown_fields():
				d[f] = _ModelBase._parse_data(field,
						show_hidden=show_hidden, use_id=use_id)
		if isinstance(self, Model):
			if use_id:
				d['id'] = self.id and str(self.id)
			elif self.id:
				d['_id'] = ObjectId(self.id)
		return d
	def __repr__(self):
		"Prints out type, id if available, and 40 characters of _fields"
		body = '; '.join(['%s: %s' %(k, getattr(self, k))
			for k in self._fields])
		if len(body) > 43: body = body[:40] + '...'
		return '<%s[%s]:{%s}>' % (self.__class__.__name__, self.id, body)
	
class Model(_ModelBase):
	@classmethod
	def find(cls, *args, **kwargs):
		"Takes a set of keywords and/or dictionary relating to object _fields"
		if len(args) and type(args[0]) == dict: kwargs.update(args[0])
		cursor = DB.connection[cls.__name__.lower()].find(kwargs)
		return Cursor(cls, cursor, kwargs)
	@classmethod
	def get(cls, id=None, **kwargs):
		"Takes an objectid or dict and returns one result"
		if not id: id = {}
		if type(id) == dict:
			id.update(kwargs)
		else:
			id = ObjectId(id)
		data = DB.connection[cls.__name__.lower()].find_one(id)
		if not data: return None
		return cls(**dict_unicode(data))
	def save(self):
		"Insert/update object into database (based on id)"
		self._id = DB.connection[self.__class__.__name__.lower()
				].save(self.to_data(show_hidden=True, use_id=False))
		return self
	@property
	def id(self):
		"Read-only access to the object id once object is saved"
		return hasattr(self, '_id') and self._id or None
	def delete(self):
		"Remove object from database"
		if self.id:
			DB.connection[self.__class__.__name__.lower()].remove(self.id)
	@property
	def dbref(self):
		if self.id:
			return DBRef(self.__class__.__name__.lower(),
				self.id, DB.connection.name)
		else: return None

class SubModel(_ModelBase):
	@property
	def id(self): return 'N/A'
